package org.apache.solr.core;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.NativeFSLockFactory;
import org.apache.lucene.store.NoLockFactory;
import org.apache.lucene.store.SimpleFSLockFactory;
import org.apache.lucene.store.SingleInstanceLockFactory;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.NamedList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A {@link DirectoryFactory} impl base class for caching Directory instances
 * per path. Most DirectoryFactory implementations will want to extend this
 * class and simply implement {@link DirectoryFactory#create(String)}.
 * 
 */
public abstract class CachingDirectoryFactory extends DirectoryFactory {

    protected class CacheValue {

        public Directory directory;
        public int refCnt = 1;
        public String path;
        public boolean doneWithDir = false;

        @Override
        public String toString() {
            return "CachedDir<<" + directory.toString() + ";refCount=" + refCnt + ";path=" + path + ";done=" + doneWithDir + ">>";
        }
    }
    private static Logger log = LoggerFactory.getLogger(CachingDirectoryFactory.class);
    protected Map<String, CacheValue> byPathCache = new HashMap<>();
    protected Map<Directory, CacheValue> byDirectoryCache = new HashMap<>();
    protected Map<Directory, List<CloseListener>> closeListeners = new HashMap<>();

    public interface CloseListener {
        public void onClose();
    }

    @Override
    public void addCloseListener(Directory dir, CloseListener closeListener) {

        synchronized (this) {
            if (!byDirectoryCache.containsKey(dir)) {
                throw new IllegalArgumentException("Unknown directory: " + dir + " " + byDirectoryCache);
            }
            List<CloseListener> listeners = closeListeners.get(dir);
            if (listeners == null) {
                listeners = new ArrayList<>();
                closeListeners.put(dir, listeners);
            }
            listeners.add(closeListener);

            closeListeners.put(dir, listeners);
        }
    }

    @Override
    public void doneWithDirectory(Directory directory) throws IOException {

        synchronized (this) {
            CacheValue cacheValue = byDirectoryCache.get(directory);
            if (cacheValue == null) {
                throw new IllegalArgumentException("Unknown directory: " + directory + " " + byDirectoryCache);
            }
            cacheValue.doneWithDir = true;
            if (cacheValue.refCnt == 0) {
                cacheValue.refCnt++; // this will go back to 0 in close
                close(directory);
            }
        }
    }

    /*
     * @see org.apache.solr.core.DirectoryFactory#close()
     */
    @Override
    public void close() throws IOException {

        synchronized (this) {
            for (CacheValue val : byDirectoryCache.values()) {
                try {
                    val.directory.close();
                }
                catch (Throwable t) {
                    SolrException.log(log, "Error closing directory", t);
                }
            }
            byDirectoryCache.clear();
            byPathCache.clear();
        }
    }

    private void close(Directory directory) throws IOException {

        synchronized (this) {
            CacheValue cacheValue = byDirectoryCache.get(directory);
            if (cacheValue == null) {
                throw new IllegalArgumentException("Unknown directory: " + directory + " " + byDirectoryCache);
            }

            log.debug("Closing: {}", cacheValue);

            cacheValue.refCnt--;
            if (cacheValue.refCnt == 0 && cacheValue.doneWithDir) {
                log.info("Closing directory:" + cacheValue.path);
                directory.close();
                byDirectoryCache.remove(directory);
                byPathCache.remove(cacheValue.path);
                List<CloseListener> listeners = closeListeners.remove(directory);
                if (listeners != null) {
                    for (CloseListener listener : listeners) {
                        listener.onClose();
                    }
                    closeListeners.remove(directory);
                }
            }
        }
    }

    @Override
    protected abstract Directory create(String path) throws IOException;

    @Override
    public boolean exists(String path) {
        // back compat behavior
        File dirFile = new File(path);
        return dirFile.canRead() && dirFile.list().length > 0;
    }

    /*
     * @see org.apache.solr.core.DirectoryFactory#get(java.lang.String,
     * java.lang.String)
     */
    @Override
    public final Directory get(String path, String rawLockType) throws IOException {
        return get(path, rawLockType, false);
    }

    /*
     * @see org.apache.solr.core.DirectoryFactory#get(java.lang.String,
     * java.lang.String, boolean)
     */
    @Override
    public final Directory get(String path, String rawLockType, boolean forceNew) throws IOException {

        String fullPath = new File(path).getAbsolutePath();
        synchronized (this) {
            CacheValue cacheValue = byPathCache.get(fullPath);
            Directory directory = null;
            if (cacheValue != null) {
                directory = cacheValue.directory;
                if (forceNew) {
                    cacheValue.doneWithDir = true;
                    if (cacheValue.refCnt == 0) {
                        close(cacheValue.directory);
                    }
                }
            }

            if (directory == null || forceNew) {
                directory = create(fullPath);

                CacheValue newCacheValue = new CacheValue();
                newCacheValue.directory = directory;
                newCacheValue.path = fullPath;

                injectLockFactory(directory, path, rawLockType);

                byDirectoryCache.put(directory, newCacheValue);
                byPathCache.put(fullPath, newCacheValue);
                log.info("return new directory for " + fullPath + " forceNew:" + forceNew);
            }
            else {
                cacheValue.refCnt++;
            }

            return directory;
        }
    }

    /*
     * @see
     * org.apache.solr.core.DirectoryFactory#incRef(org.apache.lucene.store.Directory
     * )
     */
    @Override
    public void incRef(Directory directory) {

        synchronized (this) {
            CacheValue cacheValue = byDirectoryCache.get(directory);
            if (cacheValue == null) {
                throw new IllegalArgumentException("Unknown directory: " + directory);
            }

            cacheValue.refCnt++;
        }
    }

    @Override
    public void init(NamedList args) {
    }

    /*
     * @see
     * org.apache.solr.core.DirectoryFactory#release(org.apache.lucene.store.Directory
     * )
     */
    @Override
    public void release(Directory directory) throws IOException {
        if (directory == null) {
            throw new NullPointerException();
        }
        close(directory);
    }

    private static Directory injectLockFactory(Directory dir, String lockPath, String rawLockType) throws IOException {

        if (null == rawLockType) {
            // we default to "simple" for backwards compatibility
            log.warn("No lockType configured for " + dir + " assuming 'simple'");
            rawLockType = "simple";
        }
        final String lockType = rawLockType.toLowerCase(Locale.ROOT).trim();
        switch (lockType) {
            case "simple":
                // multiple SimpleFSLockFactory instances should be OK
                dir.setLockFactory(new SimpleFSLockFactory(lockPath));
                break;
            case "native":
                dir.setLockFactory(new NativeFSLockFactory(lockPath));
                break;
            case "single":
                if (!(dir.getLockFactory() instanceof SingleInstanceLockFactory)) {
                    dir.setLockFactory(new SingleInstanceLockFactory());
                }
                break;
            case "none":
                // Recipe for disaster
                log.error("CONFIGURATION WARNING: locks are disabled on " + dir);
                dir.setLockFactory(NoLockFactory.getNoLockFactory());
                break;
            default:
                throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Unrecognized lockType: " + rawLockType);
        }
        return dir;
    }
}
